「Yes!終於完成了!!」Wayne 做完 Todo List 之後開心地舉起雙手大聲歡呼。
「如何?寫 Angular 很好玩吧?!」我笑著說。
「好玩好玩!看似複雜的功能,寫起來卻比想像中的簡單多了!」Wayne 還沉浸在剛剛的喜悅當中笑著說道。
「是不是!!哥推薦的絕對是好物阿!!」我信心十足地說道。
「不過話說回來,我有一個問題。為什麼我只要在 Component 的 constructor
函式裡加上欲使用的 Service 當參數之後就可以直接用阿?!」Wayne 好奇地問。
「哦~~~ 這是因為...」
在 Angular 裡,Service 其實一個滿核心的元件,任何你在應用程式裡會需要用到的值、函式或是功能都會用到它。而它也通常是一個有著明確定義的 Class,專門用來處理某件事。
而 Angular 之所以會有 Service 這個元件是因為它想要讓我們在使用 Angular 撰寫應用時,能夠寫的更模組化一點、重用性更高一點。
所以在理想狀況下, Component 只負責處理畫面、資料綁定,而 Service 則負責像是取得資料、表單驗證等等的邏輯處理,來讓我們的應用程式擁有更好的結構與彈性。
那 Angular 是怎麼讓 Component 能夠很便利的使用 Service 呢?
答案是:DI (Dependency Injection,依賴注入)
在物件導向的程式設計中,有一種設計原則叫做 IoC (Inversion of Control,控制反轉) ,用來減低程式碼之間的耦合度,其中最常見的方式就是使用 DI 。
有興趣的邦友可以 Google 一下上述關鍵字,很多相關資料都可以閱讀!
Angular 實作了 DI 這個設計模式,讓我們能夠很簡單地在 Component 當中注入所需要的 Service 並使用它。
正如之前所提到過的,Angular 的元件都有其相對應的裝飾器如 @NgModule
、 @Component
、 @Directive
、 @Pipe
, Service 也不例外。
那 Service 的裝飾器叫什麼呢? 當然是 Service 的裝飾器叫 @Service
阿!!@Injectable
。
而 Angular 在這裡有一個核心的機制叫做 Injector (注射器) 。
Injection 、 Injectable 與 Injector ,英文真的很有趣。
這個機制大概是這樣的運作的:
當我們像上圖一樣使用 constructor(private service: HeroService) { }
注入 Service 到 Component 裡的時候
Angular 會到注射器裡去找看看之前有沒有創建過這個 Service 的實體
如果沒有的話,注射器會自動創建一個該 Service 的實體
然後 Angular 就會把這個 Service 實體注入到該 Component裡。
但如果 Angular 在注射器有找到之前創建過的實體,則會使用同一個實體注入。
透過這種方式,我們可以很簡單地在不同 Component 使用同一個 Service、取得或共享同樣的資料,因為都是同一個實體。
不過要注意的是,Service 會因註冊在不同的 Provider 中,而有著不一樣的實體。怎麼說呢?
因為 Angular 其實提供了三種註冊 Service 的方式。第一種方式最簡單,也是 Angular CLI 在產生 Service 時預設使用的方式:
@Injectable({
providedIn: 'root'
})
這種方式是在 Service 自己的 Metadata 裡加上 providedIn: 'root'
的屬性,來告訴 Angular 說:「請把我註冊在整個系統都是使用同一個實體的注射器裡」。
所以在使用這個 Service 時,整個系統就只會有一個實體,類似 Singleton 的概念。
使用這種方式有另外一個好處,就是如果這個 Service 在整個 Angular 的生命週期裡都沒有被使用到的話, Angular 在編譯的時候,會透過 Tree-Shaking 機制把這個 Service 剔除,優化我們的應用程式。
不知道 Tree-Shaking 是什麼的話,趕快點我複習
第二種方式是註冊在某個 NgModule 裡:
@NgModule({
providers: [
BackendService,
Logger
],
...
})
這種方式是告訴 Angular 說:「請把我註冊在這整個 NgModule 都用同一個實體的注射器裡」。也就是說,假設當初 A Service 是註冊在 A Module 裡,那麼在整個 A Module 裡就會使用同一個實體。
那在 B Module 裡可以用 A Service嗎? 可以。
那在 B Module 裡的 A Service 跟在 A Module 裡的 A Service 是同一個實體嗎?
根據我的實測結果,是同一個。
跟使用第一種方式有不一樣的結果嗎?貌似沒有。
所以關於這部份,我釐清之後再告訴大家!!
而第三種方式則是註冊在 Component 裡:
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
這種方式是告訴 Angular 說:「請把我註冊在這整個 Component 都用同一個實體的注射器裡」。意思是每個 Component 拿到的 Service 實體都不是同一個。
我保證,絕對不是同一個!!
There is now a new, recommended, way to register a provider, directly inside the @Injectable() decorator, using the new providedIn attribute. It accepts 'root' as a value or any module of your application. When you use 'root', your injectable will be registered as a singleton in the application, and you don’t need to add it to the providers of the root module. Similarly, if you use providedIn: UsersModule, the injectable is registered as a provider of the UsersModule without adding it to the providers of the module.
@Injectable({
providedIn: 'root'
})
export class UserService {
}
我猜使用 providers
註冊在某個 NgModule 裡面是可以有較多彈性。
官網裡面有一個沒有使用 providedIn: 'root'
的例子,HTTP interceptors
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true }
Note the multi: true option. This required setting tells Angular that HTTP_INTERCEPTORS is a token for a multiprovider that injects an array of values, rather than a single value.
根據 stackoverflow 的一個回答:
providedIn: 'root' doesn't work for interceptors
interceptors 會注入多值, @Injectable({...})
decorator 沒有辦法處理,而且 interceptors 有順序性,因此仍然會使用 NgModule providers
嗯嗯,看需求使用
大大在文章裡寫到有測試 service 是不是同一個實體
根據我的了解,一般是藉由記憶體位置來確認。若是同一個,記憶體位置會相同。
但是 javascript 無法得到記憶體位置,大大是如何確認 ?
A module 裡的 component 改變 service 的值,然後在 B module 裡的 component 印出那個值,看有沒有被修改嗎 ?
沒錯!